Go 遍历切片或数组有两种方式,一种是下标,一种是 range。二者在功能上没有区别,但在性能上会有区别吗?
1.[]int
首先看一下遍历基本类型切片时二者的性能差别,以 []int 为例。
// genRandomIntSlice 生成指定长度的随机 []int 切片
func genRandomIntSlice(n int) []int {
rand.Seed(time.Now().UnixNano())
nums := make([]int, 0, n)
for i := 0; i < n; i++ {
nums = append(nums, rand.Int())
}
return nums
}
func BenchmarkIndexIntSlice(b *testing.B) {
nums := genRandomIntSlice(1024)
for i := 0; i < b.N; i++ {
var tmp int
for k := 0; k < len(nums); k++ {
tmp = nums[k]
}
_ = tmp
}
}
func BenchmarkRangeIntSlice(b *testing.B) {
nums := genRandomIntSlice(1024)
for i := 0; i < b.N; i++ {
var tmp int
for _, num := range nums {
tmp = num
}
_ = tmp
}
}
运行测试结果如下:
go test -bench=IntSlice$ .
goos: windows
goarch: amd64
pkg: main/perf
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkIndexIntSlice-8 5043324 236.2 ns/op
BenchmarkRangeIntSlice-8 5076255 239.1 ns/op
genRandomIntSlice()
函数用于生成指定长度元素类型为 int 的切片。从最终的结果可以看到,遍历 []int 类型的切片,下标与 range 遍历性能几乎没有区别。
2.[]struct{}
那么对于稍微复杂一点的 []struct 类型呢?
type Item struct {
id int
val [1024]byte
}
func BenchmarkIndexStructSlice(b *testing.B) {
items := make([]Item, 1024)
for i := 0; i < b.N; i++ {
var tmp int
for j := 0; j < len(items); j++ {
tmp = items[j].id
}
_ = tmp
}
}
func BenchmarkRangeIndexStructSlice(b *testing.B) {
items := make([]Item, 1024)
for i := 0; i < b.N; i++ {
var tmp int
for k := range items {
tmp = items[k].id
}
_ = tmp
}
}
func BenchmarkRangeStructSlice(b *testing.B) {
items := make([]Item, 1024)
for i := 0; i < b.N; i++ {
var tmp int
for _, item := range items {
tmp = item.id
}
_ = tmp
}
}
运行测试结果如下:
go test -bench=. -gcflags=-N -benchmem main/range
goos: darwin
goarch: amd64
pkg: main/range
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkIndexStructSlice-12 556436 2165 ns/op 1 B/op 0 allocs/op
BenchmarkRangeIndexStructSlice-12 535705 2124 ns/op 1 B/op 0 allocs/op
BenchmarkRangeStructSlice-12 38799 30914 ns/op 27 B/op 0 allocs/op
PASS
ok main/range 5.097s
可以看出,两种通过 index 遍历 []struct 性能没有差别,但是 range 遍历 []struct 时,性能非常差。
range 只遍历 []struct 下标时,性能比 range 遍历 []struct 值好很多。从这里我们应该能够知道二者性能差别之大的原因。
Item 是一个结构体类型 ,Item 由两个字段构成,一个类型是 int,一个是类型是 [1024]byte,如果每次遍历 []Item,都会进行一次值拷贝,所以带来了性能损耗。
此外,因 range 时获取的是值拷贝,对副本的修改,是不会影响到原切片。
需要注意的时,上面运行基准测试时,使用编译选项-gcflags=-N
禁用了编译器对切片遍历的优化。如果没有该选项,那么上面三种遍历方式没有性能差别。
为什么编译器会对上面的测试代码进行优化呢?因为代码实际上只取最后一个切片元素的值,所以前面的循环操作可以跳过,这样便带来性能的提升。如果是下面的代码,那么编器将无法优化,必须遍历拷贝所有元素。
func BenchmarkRange1(b *testing.B) {
items := make([]Item, 1024)
tmps := make([]int, 1024)
for i := 0; i < b.N; i++ {
for j := range items {
tmps[j] = items[j].id
}
}
}
func BenchmarkRange2(b *testing.B) {
items := make([]Item, 1024)
tmps := make([]int, 1024)
for i := 0; i < b.N; i++ {
for j, item := range items {
tmps[j] = item.id
}
}
}
无需去除编译器优化,基准测试结果如下:
go test -bench=BenchmarkRange -benchmem main/range
goos: darwin
goarch: amd64
pkg: main/range
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkRange1-12 2290372 534.8 ns/op 0 B/op 0 allocs/op
BenchmarkRange2-12 46161 27169 ns/op 22 B/op 0 allocs/op
PASS
ok main/range 3.378s
3.[]*struct
如果切片中是指向结构体的指针,而不是结构体呢?
// genItems 生成指定长度 []*Item 切片
func genItems(n int) []*Item {
items := make([]*Item, 0, n)
for i := 0; i < n; i++ {
items = append(items, &Item{id: i})
}
return items
}
func BenchmarkIndexPointer(b *testing.B) {
items := genItems(1024)
for i := 0; i < b.N; i++ {
var tmp int
for k := 0; k < len(items); k++ {
tmp = items[k].id
}
_ = tmp
}
}
func BenchmarkRangePointer(b *testing.B) {
items := genItems(1024)
for i := 0; i < b.N; i++ {
var tmp int
for _, item := range items {
tmp = item.id
}
_ = tmp
}
}
执行性能测试结果:
go test -bench=Pointer$ main/perf
goos: windows
goarch: amd64
pkg: main/perf
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkIndexPointer-8 773634 1521 ns/op
BenchmarkRangePointer-8 752077 1514 ns/op
切片元素从结构体 Item 替换为指针 *Item 后,for 和 range 的性能几乎是一样的。而且使用指针还有另一个好处,可以直接修改指针对应的结构体的值。
4.小结
range 在迭代过程中返回的是元素的拷贝,index 则不存在拷贝。
如果 range 迭代的元素较小,那么 index 和 range 的性能几乎一样,如基本类型的切片 []int。但如果迭代的元素较大,如一个包含很多属性的 struct 结构体,那么 index 的性能将显著地高于 range,有时候甚至会有上千倍的性能差异。对于这种场景,建议使用 index。如果使用 range,建议只迭代下标,通过下标访问元素,这种使用方式和 index 就没有区别了。如果想使用 range 同时迭代下标和值,则需要将切片/数组的元素改为指针,才能不影响性能。